<?php
/**
 * 路由处理类
 * @author Tongle Xu <xutongle@gmail.com> 2013-9-16
 * @copyright Copyright (c) 2003-2103 tintsoft.com
 * @license http://www.tintsoft.com
 * @version $Id$
 */
class Router extends Object {
	const CACHE_KEY = 'Router.rules';
	const GET_FORMAT = 'get';
	const PATH_FORMAT = 'path';

	/**
	 *
	 * @var array 路由规则 (pattern=>route).
	 */
	public $rules = array ();

	/**
	 *
	 * @var string path模式下的URL后缀（.html）
	 */
	public $urlSuffix = '';

	/**
	 *
	 * @var boolean 显示脚本名称
	 */
	public $showScriptName = true;

	/**
	 *
	 * @var boolean 知否将参数追加到路径信息部分
	 */
	public $appendParams = true;

	/**
	 *
	 * @var string GET参数名称
	 */
	public $routeVar = 'r';

	/**
	 *
	 * @var boolean Pathinfo是否区分大小写
	 */
	public $caseSensitive = true;
	/**
	 *
	 * @var boolean whether the GET parameter values should match the
	 *      corresponding
	 *      sub-patterns in a rule before using it to create a URL. Defaults to
	 *      false, meaning
	 *      a rule will be used for creating a URL only if its route and
	 *      parameter names match the given ones.
	 *      If this property is set true, then the given parameter values must
	 *      also match the corresponding
	 *      parameter sub-patterns. Note that setting this property to true will
	 *      degrade performance.
	 * @since 1.1.0
	 */
	public $matchValue = false;
	/**
	 *
	 * @var string the ID of the cache application component that is used to
	 *      cache the parsed URL rules.
	 *      Defaults to 'cache' which refers to the primary cache application
	 *      component.
	 *      Set this property to false if you want to disable caching URL rules.
	 */
	public $cacheID = '';

	/**
	 *
	 * @var boolean 是否启用严格URL解析
	 */
	public $useStrictParsing = false;

	/**
	 *
	 * @var string URL规则实例
	 */
	public $urlRuleClass = 'CUrlRule';

	/**
	 * URL格式
	 *
	 * @var string
	 */
	private $_urlFormat = self::GET_FORMAT;

	/**
	 * URL规则实例数组
	 *
	 * @var array
	 */
	private $_rules = array ();

	/**
	 * 基础URL
	 *
	 * @var string
	 */
	private $_baseUrl;

	/**
	 * 初始化应用程序组件
	 */
	public function init() {
		$this->processRules ();
	}

	/**
	 * 处理URL规则
	 */
	protected function processRules() {
		if (empty ( $this->rules ) || $this->getUrlFormat () === self::GET_FORMAT) return;
		// 规则缓存
		if ($this->cacheID !== false && ($cache = Kernel::getApp ()->getComponent ( $this->cacheID )) !== null) {
			$hash = md5 ( serialize ( $this->rules ) );
			if (($data = $cache->get ( self::CACHE_KEY )) !== false && isset ( $data [1] ) && $data [1] === $hash) {
				$this->_rules = $data [0];
				return;
			}
		}
		// 规则处理
		foreach ( $this->rules as $pattern => $route )
			$this->_rules [] = $this->createUrlRule ( $route, $pattern );
		if (isset ( $cache )) $cache->set ( self::CACHE_KEY, array ($this->_rules,$hash ) );
	}

	/**
	 * Adds new URL rules.
	 * In order to make the new rules effective, this method must be called
	 * BEFORE
	 * {@link CWebApplication::processRequest}.
	 *
	 * @param array $rules new URL rules (pattern=>route).
	 * @param boolean $append whether the new URL rules should be appended to
	 *        the existing
	 *        ones. If false,
	 *        they will be inserted at the beginning.
	 * @since 1.1.4
	 */
	public function addRules($rules, $append = true) {
		if ($append) {
			foreach ( $rules as $pattern => $route )
				$this->_rules [] = $this->createUrlRule ( $route, $pattern );
		} else {
			$rules = array_reverse ( $rules );
			foreach ( $rules as $pattern => $route )
				array_unshift ( $this->_rules, $this->createUrlRule ( $route, $pattern ) );
		}
	}

	/**
	 * 创建一个URL规则实例
	 *
	 * @param mixed $route the route part of the rule. This could be a string or
	 *        an array
	 * @param string $pattern the pattern part of the rule
	 * @return CUrlRule the URL rule instance
	 * @since 1.1.0
	 */
	protected function createUrlRule($route, $pattern) {
		if (is_array ( $route ) && isset ( $route ['class'] ))
			return $route;
		else {
			$urlRuleClass = Kernel::import ( $this->urlRuleClass, true );
			return new $urlRuleClass ( $route, $pattern );
		}
	}

	/**
	 * 创建一个URL
	 *
	 * @param string $route 控制器和动作(如文"site/main")
	 * @param array $params 参数列表 如果键是"#"将作为锚链接
	 * @param string $ampersand 参数分隔符 默认 '&'.
	 * @return string 构造的URL
	 */
	public function createUrl($route, $params = array(), $ampersand = '&') {
		unset ( $params [$this->routeVar] );
		foreach ( $params as $i => $param )
			if ($param === null) $params [$i] = '';

		if (isset ( $params ['#'] )) {
			$anchor = '#' . $params ['#'];
			unset ( $params ['#'] );
		} else
			$anchor = '';
		$route = trim ( $route, '/' );
		foreach ( $this->_rules as $i => $rule ) {
			if (is_array ( $rule )) $this->_rules [$i] = $rule = Kernel::createComponent ( $rule );
			if (($url = $rule->createUrl ( $this, $route, $params, $ampersand )) !== false) {
				if ($rule->hasHostInfo)
					return $url === '' ? '/' . $anchor : $url . $anchor;
				else
					return $this->getBaseUrl () . '/' . $url . $anchor;
			}
		}
		return $this->createUrlDefault ( $route, $params, $ampersand ) . $anchor;
	}

	/**
	 * 基于默认配置创建一个URL
	 *
	 * @param string $route the controller and the action (e.g. article/read)
	 * @param array $params list of GET parameters
	 * @param string $ampersand the token separating name-value pairs in the
	 *        URL.
	 * @return string the constructed URL
	 */
	protected function createUrlDefault($route, $params, $ampersand) {
		if ($this->getUrlFormat () === self::PATH_FORMAT) {
			$url = rtrim ( $this->getBaseUrl () . '/' . $route, '/' );
			if ($this->appendParams) {
				$url = rtrim ( $url . '/' . $this->createPathInfo ( $params, '/', '/' ), '/' );
				return $route === '' ? $url : $url . $this->urlSuffix;
			} else {
				if ($route !== '') $url .= $this->urlSuffix;
				$query = $this->createPathInfo ( $params, '=', $ampersand );
				return $query === '' ? $url : $url . '?' . $query;
			}
		} else {
			$url = $this->getBaseUrl ();
			if (! $this->showScriptName) $url .= '/';
			if ($route !== '') {
				$url .= '?' . $this->routeVar . '=' . $route;
				if (($query = $this->createPathInfo ( $params, '=', $ampersand )) !== '') $url .= $ampersand . $query;
			} elseif (($query = $this->createPathInfo ( $params, '=', $ampersand )) !== '')
				$url .= '?' . $query;
			return $url;
		}
	}

	/**
	 * 解析用户请求
	 *
	 * @param HttpRequest $request the request application component
	 * @return string the route (controllerID/actionID) and perhaps GET
	 *         parameters in path format.
	 */
	public function parseUrl($request) {
		if ($this->getUrlFormat () === self::PATH_FORMAT) {
			$rawPathInfo = $request->getPathInfo ();
			$pathInfo = $this->removeUrlSuffix ( $rawPathInfo, $this->urlSuffix );
			foreach ( $this->_rules as $i => $rule ) {
				if (is_array ( $rule )) $this->_rules [$i] = $rule = Kernel::createComponent ( $rule );
				if (($r = $rule->parseUrl ( $this, $request, $pathInfo, $rawPathInfo )) !== false) return isset ( $_GET [$this->routeVar] ) ? $_GET [$this->routeVar] : $r;
			}
			if ($this->useStrictParsing)
				throw new Base_HttpException ( 404, 'Unable to resolve the request "' . $pathInfo . '".' );
			else
				return $pathInfo;
		} elseif (isset ( $_GET [$this->routeVar] ))
			return $_GET [$this->routeVar];
		elseif (isset ( $_POST [$this->routeVar] ))
			return $_POST [$this->routeVar];
		else
			return '';
	}

	/**
	 * Parses a path info into URL segments and saves them to $_GET and
	 * $_REQUEST.
	 *
	 * @param string $pathInfo path info
	 */
	public function parsePathInfo($pathInfo) {
		if ($pathInfo === '') return;
		$segs = explode ( '/', $pathInfo . '/' );
		$n = count ( $segs );
		for($i = 0; $i < $n - 1; $i += 2) {
			$key = $segs [$i];
			if ($key === '') continue;
			$value = $segs [$i + 1];
			if (($pos = strpos ( $key, '[' )) !== false && ($m = preg_match_all ( '/\[(.*?)\]/', $key, $matches )) > 0) {
				$name = substr ( $key, 0, $pos );
				for($j = $m - 1; $j >= 0; -- $j) {
					if ($matches [1] [$j] === '')
						$value = array ($value );
					else
						$value = array ($matches [1] [$j] => $value );
				}
				if (isset ( $_GET [$name] ) && is_array ( $_GET [$name] )) $value = array_merge ( $_GET [$name], $value );
				$_REQUEST [$name] = $_GET [$name] = $value;
			} else
				$_REQUEST [$key] = $_GET [$key] = $value;
		}
	}

	/**
	 * 基于给定的参数创建一个路径信息
	 *
	 * @param array $params list of GET parameters
	 * @param string $equal the separator between name and value
	 * @param string $ampersand the separator between name-value pairs
	 * @param string $key this is used internally.
	 * @return string the created path info
	 */
	public function createPathInfo($params, $equal, $ampersand, $key = null) {
		$pairs = array ();
		foreach ( $params as $k => $v ) {
			if ($key !== null) $k = $key . '[' . $k . ']';

			if (is_array ( $v ))
				$pairs [] = $this->createPathInfo ( $v, $equal, $ampersand, $k );
			else
				$pairs [] = urlencode ( $k ) . $equal . urlencode ( $v );
		}
		return implode ( $ampersand, $pairs );
	}

	/**
	 * 删除自定义URL后缀
	 *
	 * @param string $pathInfo path info part in the URL
	 * @param string $urlSuffix the URL suffix to be removed
	 * @return string path info with URL suffix removed.
	 */
	public function removeUrlSuffix($pathInfo, $urlSuffix) {
		if ($urlSuffix !== '' && substr ( $pathInfo, - strlen ( $urlSuffix ) ) === $urlSuffix)
			return substr ( $pathInfo, 0, - strlen ( $urlSuffix ) );
		else
			return $pathInfo;
	}

	/**
	 * 返回应用程序基础URL
	 *
	 * @return string 基础URL
	 */
	public function getBaseUrl() {
		if ($this->_baseUrl !== null)
			return $this->_baseUrl;
		else {
			if ($this->showScriptName)
				$this->_baseUrl = Kernel::getApp ()->getRequest ()->getScriptUrl ();
			else
				$this->_baseUrl = Kernel::getApp ()->getRequest ()->getBaseUrl ();
			return $this->_baseUrl;
		}
	}

	/**
	 * 设置应用程序基础URL
	 *
	 * @param string $value the base URL of the application
	 */
	public function setBaseUrl($value) {
		$this->_baseUrl = $value;
	}

	/**
	 * 返回的URL格式
	 *
	 * @return string URL格式
	 */
	public function getUrlFormat() {
		return $this->_urlFormat;
	}

	/**
	 * 设置URL格式
	 *
	 * @param string $value URL格式，可以说get或者path
	 */
	public function setUrlFormat($value) {
		if ($value === self::PATH_FORMAT || $value === self::GET_FORMAT)
			$this->_urlFormat = $value;
		else
			throw new Base_Exception ( 'Router.UrlFormat must be either "path" or "get".' );
	}
}

/**
 * CBaseUrlRule is the base class for a URL rule class.
 *
 * Custom URL rule classes should extend from this class and implement two
 * methods:
 * {@link createUrl} and {@link parseUrl}.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.web
 * @since 1.1.8
 */
abstract class CBaseUrlRule {
	/**
	 *
	 * @var boolean whether this rule will also parse the host info part.
	 *      Defaults to false.
	 */
	public $hasHostInfo = false;
	/**
	 * 创建URL
	 *
	 * @param CUrlManager $manager the manager
	 * @param string $route the route
	 * @param array $params list of parameters (name=>value) associated with the
	 *        route
	 * @param string $ampersand the token separating name-value pairs in the
	 *        URL.
	 * @return mixed the constructed URL. False if this rule does not apply.
	 */
	abstract public function createUrl($manager, $route, $params, $ampersand);
	/**
	 * 解析URL
	 *
	 * @param CUrlManager $manager the URL manager
	 * @param CHttpRequest $request the request object
	 * @param string $pathInfo path info part of the URL (URL suffix is already
	 *        removed based
	 *        on {@link CUrlManager::urlSuffix})
	 * @param string $rawPathInfo path info that contains the potential URL
	 *        suffix
	 * @return mixed the route that consists of the controller ID and action ID.
	 *         False if this rule does not apply.
	 */
	abstract public function parseUrl($manager, $request, $pathInfo, $rawPathInfo);
}

/**
 * CUrlRule represents a URL formatting/parsing rule.
 *
 * It mainly consists of two parts: route and pattern. The former classifies
 * the rule so that it only applies to specific controller-action route.
 * The latter performs the actual formatting and parsing role. The pattern
 * may have a set of named parameters.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.web
 * @since 1.0
 */
class CUrlRule extends CBaseUrlRule {
	/**
	 *
	 * @var string URL后缀
	 */
	public $urlSuffix;
	/**
	 *
	 * @var boolean whether the rule is case sensitive. Defaults to null,
	 *      meaning
	 *      using the value of {@link CUrlManager::caseSensitive}.
	 */
	public $caseSensitive;
	/**
	 *
	 * @var array the default GET parameters (name=>value) that this rule
	 *      provides.
	 *      When this rule is used to parse the incoming request, the values
	 *      declared in this property
	 *      will be injected into $_GET.
	 */
	public $defaultParams = array ();
	/**
	 *
	 * @var boolean whether the GET parameter values should match the
	 *      corresponding
	 *      sub-patterns in the rule when creating a URL. Defaults to null,
	 *      meaning using the value
	 *      of {@link CUrlManager::matchValue}. When this property is false, it
	 *      means
	 *      a rule will be used for creating a URL if its route and parameter
	 *      names match the given ones.
	 *      If this property is set true, then the given parameter values must
	 *      also match the corresponding
	 *      parameter sub-patterns. Note that setting this property to true will
	 *      degrade performance.
	 * @since 1.1.0
	 */
	public $matchValue;
	/**
	 *
	 * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should
	 *      match.
	 *      If this rule can match multiple verbs, please separate them with
	 *      commas.
	 *      If this property is not set, the rule can match any verb.
	 *      Note that this property is only used when parsing a request. It is
	 *      ignored for URL creation.
	 * @since 1.1.7
	 */
	public $verb;
	/**
	 *
	 * @var boolean whether this rule is only used for request parsing.
	 *      Defaults to false, meaning the rule is used for both URL parsing and
	 *      creation.
	 * @since 1.1.7
	 */
	public $parsingOnly = false;
	/**
	 *
	 * @var string the controller/action pair
	 */
	public $route;
	/**
	 *
	 * @var array the mapping from route param name to token name (e.g.
	 *      _r1=><1>)
	 */
	public $references = array ();
	/**
	 *
	 * @var string the pattern used to match route
	 */
	public $routePattern;
	/**
	 *
	 * @var string regular expression used to parse a URL
	 */
	public $pattern;
	/**
	 *
	 * @var string template used to construct a URL
	 */
	public $template;
	/**
	 *
	 * @var array list of parameters (name=>regular expression)
	 */
	public $params = array ();
	/**
	 *
	 * @var boolean whether the URL allows additional parameters at the end of
	 *      the path info.
	 */
	public $append;
	/**
	 *
	 * @var boolean whether host info should be considered for this rule
	 */
	public $hasHostInfo;

	/**
	 * Constructor.
	 *
	 * @param string $route the route of the URL (controller/action)
	 * @param string $pattern the pattern for matching the URL
	 */
	public function __construct($route, $pattern) {
		if (is_array ( $route )) {
			foreach ( array ('urlSuffix','caseSensitive','defaultParams','matchValue','verb','parsingOnly' ) as $name ) {
				if (isset ( $route [$name] )) $this->$name = $route [$name];
			}
			if (isset ( $route ['pattern'] )) $pattern = $route ['pattern'];
			$route = $route [0];
		}
		$this->route = trim ( $route, '/' );

		$tr2 ['/'] = $tr ['/'] = '\\/';

		if (strpos ( $route, '<' ) !== false && preg_match_all ( '/<(\w+)>/', $route, $matches2 )) {
			foreach ( $matches2 [1] as $name )
				$this->references [$name] = "<$name>";
		}

		$this->hasHostInfo = ! strncasecmp ( $pattern, 'http://', 7 ) || ! strncasecmp ( $pattern, 'https://', 8 );

		if ($this->verb !== null) $this->verb = preg_split ( '/[\s,]+/', strtoupper ( $this->verb ), - 1, PREG_SPLIT_NO_EMPTY );

		if (preg_match_all ( '/<(\w+):?(.*?)?>/', $pattern, $matches )) {
			$tokens = array_combine ( $matches [1], $matches [2] );
			foreach ( $tokens as $name => $value ) {
				if ($value === '') $value = '[^\/]+';
				$tr ["<$name>"] = "(?P<$name>$value)";
				if (isset ( $this->references [$name] ))
					$tr2 ["<$name>"] = $tr ["<$name>"];
				else
					$this->params [$name] = $value;
			}
		}
		$p = rtrim ( $pattern, '*' );
		$this->append = $p !== $pattern;
		$p = trim ( $p, '/' );
		$this->template = preg_replace ( '/<(\w+):?.*?>/', '<$1>', $p );
		$this->pattern = '/^' . strtr ( $this->template, $tr ) . '\/';
		if ($this->append)
			$this->pattern .= '/u';
		else
			$this->pattern .= '$/u';

		if ($this->references !== array ()) $this->routePattern = '/^' . strtr ( $this->route, $tr2 ) . '$/u';

		if (YII_DEBUG && @preg_match ( $this->pattern, 'test' ) === false) throw new Base_Exception ( Yii::t ( 'yii', 'The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', array ('{route}' => $route,'{pattern}' => $pattern ) ) );
	}

	/**
	 * Creates a URL based on this rule.
	 *
	 * @param CUrlManager $manager the manager
	 * @param string $route the route
	 * @param array $params list of parameters
	 * @param string $ampersand the token separating name-value pairs in the
	 *        URL.
	 * @return mixed the constructed URL or false on error
	 */
	public function createUrl($manager, $route, $params, $ampersand) {
		if ($this->parsingOnly) return false;

		if ($manager->caseSensitive && $this->caseSensitive === null || $this->caseSensitive)
			$case = '';
		else
			$case = 'i';

		$tr = array ();
		if ($route !== $this->route) {
			if ($this->routePattern !== null && preg_match ( $this->routePattern . $case, $route, $matches )) {
				foreach ( $this->references as $key => $name )
					$tr [$name] = $matches [$key];
			} else
				return false;
		}

		foreach ( $this->defaultParams as $key => $value ) {
			if (isset ( $params [$key] )) {
				if ($params [$key] == $value)
					unset ( $params [$key] );
				else
					return false;
			}
		}

		foreach ( $this->params as $key => $value )
			if (! isset ( $params [$key] )) return false;

		if ($manager->matchValue && $this->matchValue === null || $this->matchValue) {
			foreach ( $this->params as $key => $value ) {
				if (! preg_match ( '/\A' . $value . '\z/u' . $case, $params [$key] )) return false;
			}
		}

		foreach ( $this->params as $key => $value ) {
			$tr ["<$key>"] = urlencode ( $params [$key] );
			unset ( $params [$key] );
		}

		$suffix = $this->urlSuffix === null ? $manager->urlSuffix : $this->urlSuffix;

		$url = strtr ( $this->template, $tr );

		if ($this->hasHostInfo) {
			$hostInfo = Kernel::getApp ()->getRequest ()->getHostInfo ();
			if (stripos ( $url, $hostInfo ) === 0) $url = substr ( $url, strlen ( $hostInfo ) );
		}

		if (empty ( $params )) return $url !== '' ? $url . $suffix : $url;

		if ($this->append)
			$url .= '/' . $manager->createPathInfo ( $params, '/', '/' ) . $suffix;
		else {
			if ($url !== '') $url .= $suffix;
			$url .= '?' . $manager->createPathInfo ( $params, '=', $ampersand );
		}

		return $url;
	}
	public function parseUrl($manager, $request, $pathInfo, $rawPathInfo) {
		if ($this->verb !== null && ! in_array ( $request->getRequestType (), $this->verb, true )) return false;

		if ($manager->caseSensitive && $this->caseSensitive === null || $this->caseSensitive)
			$case = '';
		else
			$case = 'i';

		if ($this->urlSuffix !== null) $pathInfo = $manager->removeUrlSuffix ( $rawPathInfo, $this->urlSuffix );
		if ($manager->useStrictParsing && $pathInfo === $rawPathInfo) {
			$urlSuffix = $this->urlSuffix === null ? $manager->urlSuffix : $this->urlSuffix;
			if ($urlSuffix != '' && $urlSuffix !== '/') return false;
		}
		if ($this->hasHostInfo) $pathInfo = strtolower ( $request->getHostInfo () ) . rtrim ( '/' . $pathInfo, '/' );
		$pathInfo .= '/';
		if (preg_match ( $this->pattern . $case, $pathInfo, $matches )) {
			foreach ( $this->defaultParams as $name => $value ) {
				if (! isset ( $_GET [$name] )) $_REQUEST [$name] = $_GET [$name] = $value;
			}
			$tr = array ();
			foreach ( $matches as $key => $value ) {
				if (isset ( $this->references [$key] ))
					$tr [$this->references [$key]] = $value;
				elseif (isset ( $this->params [$key] ))
					$_REQUEST [$key] = $_GET [$key] = $value;
			}
			if ($pathInfo !== $matches [0]) $manager->parsePathInfo ( ltrim ( substr ( $pathInfo, strlen ( $matches [0] ) ), '/' ) );
			if ($this->routePattern !== null)
				return strtr ( $this->route, $tr );
			else
				return $this->route;
		} else
			return false;
	}
}
